import numpy as np
import json
import matplotlib.pyplot as plt


def load_data():
    '''
    数据处理代码的封装
    :return: 返回处理好的训练集数据，测试集数据
    '''
    # 读入训练数据
    datafile = '../work/housing.data'
    data = np.fromfile(datafile, sep=' ')  # 1维的数据：1*n
    # print(data.shape)  # (7084,)
    # 训练数据的处理
    '''
    因为读入的原始数据是1维的，所有数据都连在了一起。
    所以将数据的形状进行变换，形成一个2维的矩阵。每行为一个数据样本（14个值），
    每个数据样本包含13个X（影响房价的特征）和一个Y（该类型房屋的均价）。
    读入之后的数据被转化成1维array，其中array的
    第0-13项是第一条数据，第14-27项是第二条数据，....
    这里对原始数据做reshape，变成N x 14的形式
    '''
    # 1、13个特征的名称+1个标签的名称
    feature_names = [ 'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE','DIS',
                     'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV' ]
    feature_num = len(feature_names)  # 14 = 13+特征值长度+1标签的长度
    data = data.reshape([data.shape[0] // feature_num, feature_num])
    # print(data.shape)  # (506, 14)
    # x = data[0]
    # print(x.shape)  # (14,)
    # print(x)

    '''
    取80%的数据作为训练集，预留20%的数据用于测试模型的预测效果（训练好的模型预测值与实际房价的差距）。
    打印训练集的形状可见，我们共有404个样本，每个样本含有13个特征和1个预测值。
    '''
    ratio = 0.8
    offset = int(data.shape[0] * ratio)
    training_data = data[: offset]  # 其实这里可以更好的处理一下，每10组数据选取前8组数据等方法。
    # print(training_data.shape) # (404, 14)

    '''
    对每个特征进行归一化处理，使得每个特征的取值缩放到0~1之间。这样做有两个好处：
    1、模型训练更高效。
    2、特征前的权重大小可代表该变量对预测结果的贡献度（因为每个特征值本身的范围相同）。
    归一化后的数据 = （样本数据 - 均值） / （最大值 - 最小值）
    '''
    # 计算train数据集的最大值，最小值，平均值
    '''
    1.ndarray.max([int axis])
    函数功能：求ndarray中指定维度的最大值，默认求所有值的最大值。
    axis=0:求各column的最大值
    axis=1:求各row的最大值
    '''
    maximums, minimums, avgs = training_data.max(axis=0), training_data.min(axis=0),\
                               training_data.sum(axis=0) / training_data.shape[0]
    # 对数据进行归一化处理
    # 注意：预测时，样本数据同样也需要归一化，以训练样本的均值和极值计算
    # 这里为什么以训练集样本的均值和极值作为归一化的数据，而不是以整体样本均值和极值的数据呢？
    # 因为在训练过程中模拟真实，只能让机器知道训练集的数据，对于测试集的数据机器是完全不知道的，
    # 所以对于不知道的数据就无法知道他的极值和均值，只能用训练集样本的极值和均值来作为归一化的数据。
    for i in range(feature_num):
        data[:, i] = (data[:, i] - avgs[i]) / (maximums[i] - minimums[i])  # 一共有14列，每一列的数据都进行归一化计算

    # 训练集和测试集的划分比例
    training_data = data[: offset]
    test_data = data[offset:]
    return training_data, test_data

# 获取数据
training_data, test_data = load_data()
x = training_data[:, : -1]  # x 前13个特征值
y = training_data[:, -1:]  # y 最后一个标签值
# print(x[0])
# print(y[0])
'''
如果将输入特征和输出预测值均以向量表示，输入特征x一共有13个分量，y只有1个分量，所以参数权重的形状（shape）应该是13×113\times113×1。假设我们以如下任意数字赋值参数做初始化： w=[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,−0.1,−0.2,−0.3，−0.4,0.0]w=[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, -0.1, -0.2, -0.3，-0.4, 0.0]
w=[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,−0.1,−0.2,−0.3，−0.4,0.0]
'''
# w = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, -0.1, -0.2, -0.3, -0.4, 0.0]
# w = np.array(w).reshape([13, 1])
# 取出第1条样本数据，观察样本的特征向量与参数向量相乘之后的结果。
# x1 = x[0]
# t = np.dot(x1, w)
# print(t)
# # 此外，完整的线性回归公式，还需要初始化偏移量bbb，同样随意赋初值-0.2。 那么，线性回归模型的完整输出是z=t+bz=t+bz=t+b，这个从特征和参数计算输出值的过程称为“前向计算”。
# b = -0.2
# z = t + b
# print(z)
'''
构建神经网络
将上述计算预测输出的过程以“类和对象”的方式来描述，实现的方案如下所示。
类成员变量有参数 w 和 b，并写了一个forward函数（代表“前向计算”）完成上述从特征和参数到输出预测值的计算过程。
'''
class Network(object):
    # 初始化参数数据
    def __init__(self, num_of_weights):
        # 随机产生w的初始值
        # 为了保持程序每次寻星结果的一致性，此处设置固定的随机种子
        np.random.seed(0)
        self.w = np.random.randn(num_of_weights, 1)
        self.b = 0.

    # 设置前向传播网络
    def forward(self, x):
        z = np.dot(x, self.w) + self.b  # 线性回归的模型
        return z

    # 定义损失函数
    def loss(self, z, y):
        error = z - y  # 预测所有结果z向量 - 真实结果y向量
        cost = error * error
        cost = np.mean(cost)
        return cost

    # 定义优化函数
    def gradient(self, x, y):
        z = self.forward(x)
        gradient_w = (z - y) * x
        gradient_w = np.mean(gradient_w, axis=0)
        gradient_w = gradient_w[:, np.newaxis]  # 目的是为了使梯度下降的参数与原来参数的shape一样，才能进行+-运算
        gradient_b = (z - y)
        gradient_b = np.mean(gradient_b)

        return gradient_w, gradient_b

    # 更新参数
    def update(self, gradient_w, gradient_b, eta=0.01):
        """

        :param gradient_w: 梯度下降的参数
        :param gradient_b: 偏置值
        :param eta: 学习率
        :return: 更新后的参数
        """
        self.w = self.w - eta * gradient_w
        self.b = self.b - eta * gradient_b

    # 训练数据
    def train(self, x, y, iterations=100, eta = 0.01):
        losses = []
        for i in range(iterations):
            z = self.forward(x)  # 1、向前传播计算预测值
            L = self.loss(z, y)  # 2、计算预测值与真实值的误差
            gradient_w, gradient_b = self.gradient(x, y)  # 3、使用梯度下降法求出下降的斜率
            self.update(gradient_w, gradient_b) # 4、更新参数，使得数据更好的拟合真实值，目的：下降的斜率接近0
            losses.append(L)
            if (i +1) % 10 == 0:
                print('iter {}, loss {}'.format(i, L)) # 每隔10次打印一次损失值
        return losses


# 基于Network类的定义，模型的计算过程可以按下述方式达成。
# net = Network(13)
# x1 = x[0]
# y1 = y[0]
# z = net.forward(x1)
# print(z)

'''
通过模型计算x[0]表示的影响因素所对应的房价应该是z, 但实际数据告诉我们房价是y，
这时我们需要有某种指标来衡量预测值z跟真实值y之间的差距。对于回归问题，
最常采用的衡量方法是使用均方误差作为评价模型好坏的指标，具体定义如下：
Loss = (y - z)^2
上式中的Loss(简记为: L) 通常也被称作损失函数，它是衡量模型好坏的指标，
在回归问题中均方误差是一种比较常见的形式，分类问题中通常会采用交叉熵损失函数，在后续的章节中会更详细的介绍。
对一个样本计算损失的代码实现如下：
'''
# Loss = (y1 - z) * (y1 - z)
# print(Loss)
'''
因为计算损失时需要把每个样本的损失都考虑到，所以我们需要对单个样本的损失函数进行求和，并除以样本总数N。
L=1/N∑(y(i)−z(i))^2L
对上面的计算代码做出相应的调整，在Network类下面添加损失函数的计算过程如下
使用上面定义的Network类，可以方便的计算预测值和损失函数。
需要注意，类中的变量x, w，b, z, error等均是向量。
以变量x为例，共有两个维度，一个代表特征数量（=13），一个代表样本数量（演示程序如下）。
'''
# net = Network(13)
# # 此处可以一次性计算多个样本的预测值和损失函数
# x1 = x[0: 3]
# y1 = y[0: 3]
# z = net.forward(x1)
# print('predict:', z)
# loss = net.loss(z, y1)
# print('loss:', loss)


if __name__ == '__main__':
    train_data, test_data = load_data()
    x = train_data[:, : -1]
    y = train_data[:, -1:]
    # 创建网络
    net = Network(13)
    num_iterations = 10000
    # 启动训练
    losses = net.train(x, y, iterations=num_iterations, eta=0.01)

    # 画出损失函数的变化趋势
    plot_x = np.arange(num_iterations)  # x轴为迭代此时
    plot_y = np.array(losses)  # y轴为损失值
    plt.plot(plot_x, plot_y)
    plt.show()